package com.acme.javaschool.intense.sems.sem1collections;

import java.util.Arrays;

public class YetAnotherHashMap {

    private final int INITIAL_CAPACITY_POW = 6;
    private int thresholdLoad;

    private Integer[] keys;
    private Integer[] vals;

    private int capacity;
    private int capacityPow = INITIAL_CAPACITY_POW;
    private int load;
    private boolean resizeByThreshold;

    public YetAnotherHashMap() {
        thresholdLoad = 70;
        capacity = 2 << INITIAL_CAPACITY_POW;
        keys = new Integer[capacity];
        vals = new Integer[capacity];
        load = 0;
        resizeByThreshold = true;
    }

    public YetAnotherHashMap(boolean resizeByThreshold, int thresholdLoad) {
        this();
        this.thresholdLoad = thresholdLoad;
        this.resizeByThreshold = resizeByThreshold;
    }

    public YetAnotherHashMap(boolean resizeByThreshold, int thresholdLoad, int initialCapacityPow) {
        capacityPow = initialCapacityPow;
        capacity = 2 << capacityPow;
        keys = new Integer[capacity];
        vals = new Integer[capacity];
        load = 0;
        this.thresholdLoad = thresholdLoad;
        this.resizeByThreshold = resizeByThreshold;
    }

    public Integer put(Integer k, Integer v) {
        if (k == null) {
            return null;
        }
        Integer index = indexFor(hash(k));
        if (keys[index] != null) {
            if (keys[index].equals(k)) {
                return addEntry(index, k, v);
            }
            index = findKey(k, false);
        }

        //TODO: Implement backward resizing?
        if ((index == null) || ((resizeByThreshold) && (load * 100. / capacity >= thresholdLoad - 1))) {
//            System.out.print("loadFactor=" + load * 100. / capacity + ", index=" + index + "; ");
            Integer[] oldKeys = keys;
            Integer[] oldVals = vals;
            capacityPow++;
            capacity <<= 1;
            load = 0;
            keys = new Integer[capacity];
            vals = new Integer[capacity];

            for (int i = 0; i < oldKeys.length; i++) {
                put(oldKeys[i], oldVals[i]);
            }

            index = indexFor(hash(k));
            if (keys[index] != null) {
                index = findKey(k, false);
            }
        }

        return addEntry(index, k, v);
    }

    /**
     * Finds either desired key or index of closest (upper, cyclic) null.
     * @param key
     * @return index if found, else null
     */
    private Integer findKey(Integer key, boolean skipNull) {
        if (key == null) {
            return null;
        }

        int originalIndex = indexFor(hash(key));
        int index = originalIndex;
        while (true){
            if (skipNull) {
                if ((keys[index] == null) || !(keys[index].equals(key))) {
                    index = (index + 1) & (capacity - 1);
                } else {
                    break;
                }
            } else if ((keys[index] == null) || (keys[index].equals(key))) {
                break;
            } else {
                index = (index + 1) & (capacity - 1);
            }

            if (index == originalIndex) {
                return null;
            }
        }
        return index;
    }

    private Integer addEntry(int index, Integer k, Integer v) {
        Integer v0 = vals[index];
        if (v0 == null) {
            load++;
        }

        keys[index] = k;
        vals[index] = v;

        return v0;
    }

    public Integer get(Integer k) {
        Integer index = findKey(k, true);
        if (index == null) {
            return null;
        }
        return vals[index];
    }

    private int standardHash(Integer k) {
        int h = 0;

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    private int strangeHash(Integer k) {
        return (hash(k) >>> 2) << 2;
    }

    private int hash(Integer k) {
        return standardHash(k);
//        return strangeHash(k);
//        return k.hashCode();
    }

    private int indexFor(int h) {
        return h & (capacity - 1);
    }

    @Override
    public String toString() {
        StringBuilder s = new StringBuilder("[");

        for (int i = 0; i < capacity; i++) {
            if (keys[i] != null) {
                s.append(keys[i] + "=" + vals[i] + ", ");
            } else {
                s.append("n=n, ");
            }
        }
        return s.append("]").toString();
    }

    public void touch() {
        Arrays.toString(keys);
        Arrays.toString(vals);
    }

    public int getCapacity() {
        return capacity;
    }

    public boolean exists(Integer key) {
        if (findKey(key, true) != null) {
            return true;
        }
        return false;
    }

    public Integer remove(Integer key) {
        Integer index = findKey(key, true);
        if (index == null) {
            return null;
        }
        int v0 = vals[index];
        load--;

        keys[index] = null;
        vals[index] = null;

        return v0;
    }
}
